#include <streambuf>
#include <string>
#include <string.h>
#include <stdexcept>
#include "physfs.hpp"

#include "tnlAssert.h"

using namespace TNL;

using std::streambuf;
using std::ios_base;

namespace PhysFS {

   class fbuf : public streambuf {
   private:
      fbuf(const fbuf & other);
      fbuf& operator=(const fbuf& other);

      int_type underflow() {
         if(PHYSFS_eof(file)) {
            return traits_type::eof();
         }
         PHYSFS_sint64 bytesRead = PHYSFS_read(file, buffer, 1, (PHYSFS_uint32)bufferSize);
         if(bytesRead < 1) {
            return traits_type::eof();
         }
         setg(buffer, buffer, buffer + bytesRead);
         return (unsigned char)*gptr();
      }

      pos_type seekoff(off_type pos, ios_base::seekdir dir, ios_base::openmode mode) {
         switch(dir) {
         case std::ios_base::beg:
            PHYSFS_seek(file, pos);
            break;
         case std::ios_base::cur:
            // subtract characters currently in buffer from seek position
            PHYSFS_seek(file, (PHYSFS_tell(file) + pos) - (egptr() - gptr()));
            break;
         case std::ios_base::end:
            PHYSFS_seek(file, PHYSFS_fileLength(file) + pos);
            break;
         }
         if(mode & std::ios_base::in) {
            setg(egptr(), egptr(), egptr());
         }
         if(mode & std::ios_base::out) {
            setp(buffer, buffer);
         }
         return PHYSFS_tell(file);
      }

      pos_type seekpos(pos_type pos, std::ios_base::openmode mode) {
         PHYSFS_seek(file, pos);
         if(mode & std::ios_base::in) {
            setg(egptr(), egptr(), egptr());
         }
         if(mode & std::ios_base::out) {
            setp(buffer, buffer);
         }
         return PHYSFS_tell(file);
      }

      int_type overflow(int_type c = traits_type::eof()) {
         if(pptr() == pbase() && c == traits_type::eof()) {
            return 0; // no-op
         }
         if(PHYSFS_write(file, pbase(), pptr() - pbase(), 1) < 1) {
            return traits_type::eof();
         }
         if(c != traits_type::eof()) {
            if(PHYSFS_write(file, &c, 1, 1) < 1) {
               return traits_type::eof();
            }
         }

         return 0;
      }

      int sync() {
         return overflow();
      }

      char * buffer;
      size_t const bufferSize;
   protected:
      PHYSFS_File * const file;
   public:
      fbuf(PHYSFS_File * file, std::size_t bufferSize = 2048) : file(file), bufferSize(bufferSize) {
         buffer = new char[bufferSize];
         char * end = buffer + bufferSize;
         setg(end, end, end);
         setp(buffer, end);
      }

      ~fbuf() {
         sync();
         delete[] buffer;
      }
   };

   base_fstream::base_fstream(PHYSFS_File* file) : file(file) {
      if(file == NULL) {
         throw std::invalid_argument("attempted to construct fstream with NULL ptr");
      }
   }

   base_fstream::~base_fstream() {
      PHYSFS_close(file);
   }

   PhysFS::size_t base_fstream::length() {
      return PHYSFS_fileLength(file);
   }

   PHYSFS_File* openWithMode(char const * filename, mode openMode) {
      PHYSFS_File* file = NULL;
      switch(openMode) {
      case WRITE:
         file = PHYSFS_openWrite(filename);
         break;
      case APPEND:
         file = PHYSFS_openAppend(filename);
         break;
      case READ:
         file = PHYSFS_openRead(filename);
      }
      if(file == NULL) {
         throw std::invalid_argument("file not found: " + std::string(filename));
      }
      return file;
   }

   ifstream::ifstream(const string& filename)
      : base_fstream(openWithMode(filename.c_str(), READ)), std::istream(new fbuf(file)) {}

   ifstream::~ifstream() {
      delete rdbuf();
   }

   ofstream::ofstream(const string& filename, mode writeMode)
      : base_fstream(openWithMode(filename.c_str(), writeMode)), std::ostream(new fbuf(file)) {}

   ofstream::~ofstream() {
      delete rdbuf();
   }

   fstream::fstream(const string& filename, mode openMode)
      : base_fstream(openWithMode(filename.c_str(), openMode)), std::iostream(new fbuf(file)) {}

   fstream::~fstream() {
      delete rdbuf();
   }

   Version getLinkedVersion() {
      Version version;
      PHYSFS_getLinkedVersion(&version);
      return version;
   }

   bool init(const char* argv0) {
      return PHYSFS_init(argv0);
   }

   void deinit() {
      PHYSFS_deinit();
   }

   ArchiveInfoList supportedArchiveTypes() {
      ArchiveInfoList list;
      for(const ArchiveInfo** archiveType = PHYSFS_supportedArchiveTypes(); *archiveType != NULL; archiveType++) {
         list.push_back(**archiveType);
      }
      return list;
   }

   string getDirSeparator() {
      return PHYSFS_getDirSeparator();
   }

   void permitSymbolicLinks(bool allow) {
      PHYSFS_permitSymbolicLinks(allow);
   }

   StringList getCdRomDirs() {
      StringList dirs;
      char ** dirBegin = PHYSFS_getCdRomDirs();
      for(char ** dir = dirBegin; *dir != NULL; dir++) {
         dirs.push_back(*dir);
      }
      PHYSFS_freeList(dirBegin);
      return dirs;
   }

   void getCdRomDirs(StringCallback callback, void * extra) {
      PHYSFS_getCdRomDirsCallback(callback, extra);
   }

   string getBaseDir() {
      return PHYSFS_getBaseDir();
   }

   string getUserDir() {
      return PHYSFS_getUserDir();
   }

   string getWriteDir() {
      return PHYSFS_getWriteDir();
   }

   void setWriteDir(const string& newDir) {
      PHYSFS_setWriteDir(newDir.c_str());
   }

   void removeFromSearchPath(const string& oldDir) {
      PHYSFS_removeFromSearchPath(oldDir.c_str());
   }

   StringList getSearchPath() {
      TNLAssert(PhysFS::isInit(), "Must initialize PhysFS first!");

      StringList pathList;
      char ** pathBegin = PHYSFS_getSearchPath();
      for(char ** path = pathBegin; *path != NULL; path++) {
         pathList.push_back(*path);
      }
      PHYSFS_freeList(pathBegin);
      return pathList;
   }

   void getSearchPath(StringCallback callback, void * extra) {
      TNLAssert(PhysFS::isInit(), "Must initialize PhysFS first!");

      PHYSFS_getSearchPathCallback(callback, extra);
   }

   void clearSearchPath() {
      StringList dirs = getSearchPath();

      // PhysFS uses a linked list to store folders, so forward traversal is the most efficient
      // (see http://libphysfs.sourcearchive.com/documentation/2.0.0/physfs_8h_1eef17d8edd5525928e97d32e3e58428.html)
      for(unsigned int i = 0; i < dirs.size(); i++)   
         removeFromSearchPath(dirs[i]);
   }

   void mountAll(const StringList &folderList) {
      for(unsigned int i = 0; i < folderList.size(); i++)
         mount(folderList[i], "", true);
   }


   void setSaneConfig(const string& orgName, const string& appName,
      const string& archiveExt, bool includeCdRoms, bool archivesFirst) {
      PHYSFS_setSaneConfig(orgName.c_str(), appName.c_str(), archiveExt.c_str(), includeCdRoms, archivesFirst);
   }

   void mkdir(const string& dirName) {
      PHYSFS_mkdir(dirName.c_str());
   }

   void deleteFile(const string& filename) {
      PHYSFS_delete(filename.c_str());
   }

   string getRealDir(const string& filename) {
      const char *res = PHYSFS_getRealDir(filename.c_str());
      if(!res)
         return "";

      return string(res);
   }

   StringList enumerateFiles(const string& directory) {
      StringList files;
      char ** listBegin = PHYSFS_enumerateFiles(directory.c_str());
      for(char ** file = listBegin; *file != NULL; file++) {
         files.push_back(*file);
      }
      PHYSFS_freeList(listBegin);
      return files;
   }

   void enumerateFiles(const string& directory, EnumFilesCallback callback, void * extra) {
      PHYSFS_enumerateFilesCallback(directory.c_str(), callback, extra);
   }

   bool exists(const string& filename) {
      return PHYSFS_exists(filename.c_str());
   }

   bool isDirectory(const string& filename) {
      return PHYSFS_isDirectory(filename.c_str());
   }

   bool isSymbolicLink(const string& filename) {
      return PHYSFS_isSymbolicLink(filename.c_str());
   }

   sint64 getLastModTime(const string& filename) {
      return PHYSFS_getLastModTime(filename.c_str());
   }

   bool isInit() {
      return PHYSFS_isInit();
   }

   bool symbolicLinksPermitted() {
      return PHYSFS_symbolicLinksPermitted();
   }

   void setAllocator(const Allocator* allocator) {
      PHYSFS_setAllocator(allocator);
   }

   void mount(const string& newDir, const string& mountPoint, bool appendToPath) {
      TNLAssert(newDir != "", "Can't mount empty folder!");
      TNLAssert(PhysFS::isInit(), "Must initialize PhysFS first!");
      PHYSFS_mount(newDir.c_str(), mountPoint.c_str(), appendToPath);
   }

   string getMountPoint(const string& dir) {
      return PHYSFS_getMountPoint(dir.c_str());
   }

   sint16 Util::swapSLE16(sint16 value) {
      return PHYSFS_swapSLE16(value);
   }

   uint16 Util::swapULE16(uint16 value) {
      return PHYSFS_swapULE16(value);
   }

   sint32 Util::swapSLE32(sint32 value) {
      return PHYSFS_swapSLE32(value);
   }

   uint32 Util::swapULE32(uint32 value) {
      return PHYSFS_swapULE32(value);
   }

   sint64 Util::swapSLE64(sint64 value) {
      return PHYSFS_swapSLE64(value);
   }

   uint64 Util::swapULE64(uint64 value) {
      return PHYSFS_swapULE64(value);
   }

   sint16 Util::swapSBE16(sint16 value) {
      return PHYSFS_swapSBE16(value);
   }

   uint16 Util::swapUBE16(uint16 value) {
      return PHYSFS_swapUBE16(value);
   }

   sint32 Util::swapSBE32(sint32 value) {
      return PHYSFS_swapSBE32(value);
   }

   uint32 Util::swapUBE32(uint32 value) {
      return PHYSFS_swapUBE32(value);
   }

   sint64 Util::swapSBE64(sint64 value) {
      return PHYSFS_swapSBE64(value);
   }

   uint64 Util::swapUBE64(uint64 value) {
      return PHYSFS_swapUBE64(value);
   }

   string Util::utf8FromUcs4(const uint32* src) {
      string value;
      std::size_t length = strlen((char*)src);
      char * buffer = new char[length]; // will be smaller than len
      PHYSFS_utf8FromUcs4(src, buffer, length);
      value.append(buffer);
      return value;
   }

   string Util::utf8ToUcs4(const char* src) {
      string value;
      std::size_t length = strlen(src) * 4;
      char * buffer = new char[length]; // will be smaller than len
      PHYSFS_utf8ToUcs4(src, (uint32*)buffer, length);
      value.append(buffer);
      return value;
   }

   string Util::utf8FromUcs2(const uint16* src) {
      string value;
      std::size_t length = strlen((char*)src);
      char * buffer = new char[length]; // will be smaller than len
      PHYSFS_utf8FromUcs2(src, buffer, length);
      value.append(buffer);
      return value;
   }

   string Util::utf8ToUcs2(const char* src) {
      string value;
      std::size_t length = strlen(src) * 2;
      char * buffer = new char[length]; // will be smaller than len
      PHYSFS_utf8ToUcs2(src, (uint16*)buffer, length);
      value.append(buffer);
      return value;
   }

   string Util::utf8FromLatin1(const char* src) {
      string value;
      std::size_t length = strlen((char*)src) * 2;
      char * buffer = new char[length]; // will be smaller than len
      PHYSFS_utf8FromLatin1(src, buffer, length);
      value.append(buffer);
      return value;
   }

}
